package services;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import model.Int;
import model.Nat;
import model.Rat;
import derivable.Conjunction;
import derivable.Disjunction;
import derivable.Operator;
import derivable.Postcondition;
import derivable.Proposition;
import derivable.Variable;

/**
 * @author Henrik Wall
 * @author Rudolf Schmidt
 * @author Eugen Nemirowski
 */
public class Directory {
	interface Service {
		/**
		 * Find the postcondition a service operation by name.
		 * 
		 * @param name the key to find the postcondition
		 * @return the postcondition or {@code null} if not found
		 */
		Disjunction getDisjunction(String name);
	}
	
	/**
	 * Provides math methods.
	 */
	class MathService implements Service {
		private final static String ABS = "abs";

		private final Map<String, Disjunction> disjunctions = new HashMap<>();

		{
			final Variable x = Variable.create("x");
			final Variable result = Variable.create("result");
			final Proposition p = Proposition.create(Operator.LESS_THAN, true, x); // !(x < 0)
			final Proposition p2 = Proposition.create(Operator.EQUALS, false, result, x); // (result = x)
			final Proposition p3 = Proposition.create(Operator.LESS_THAN, false, x); // (x < 0)
			final Proposition p4 = Proposition.create(Operator.NEGATIVE_EQUALS, false, result, x); // (result -= x)
			final Conjunction c = Conjunction.create(p, p2); // (!(x < 0) & (result = x))
			final Conjunction c2 = Conjunction.create(p3, p4); // ((x < 0) & (result = -x))
			final Disjunction d = Disjunction.create(c, c2); // ((!(x < 0) & (result = x)) | ((x < 0) & (result = -x)))
			disjunctions.put(ABS, d);
		}

		@Override
		public final Disjunction getDisjunction(final String name) {
			return disjunctions.get(name);
		}

		/**
		 * Returns the absolute value of an {@link Int}.
		 * 
		 * @throws IllegalArgumentException
		 *             If the wrapped {@code long} of the argument is equal to
		 *             the value of {@link Long#MIN_VALUE}, the most negative
		 *             representable {@code long} value. This is caused by the
		 *             fact that there is no correspondent positive value in the
		 *             range of {@code long}.
		 */
		@Postcondition(ABS)
		public Nat abs(final Int x) {
			return Nat.create(Math.abs(x.longValue()));
		}

		/**
		 * Rounds a {@link Rat} to the closest {@link Int}.
		 * 
		 * @param x
		 *            the rational number to be rounded
		 * @return the closest integer
		 */
		public Int round(final Rat x) {
			return Int.create(Math.round(x.doubleValue()));
		}

		/**
		 * Rounds a {@link Rat} to a specified number of decimal places.
		 * 
		 * @param x
		 *            the rational number to be rounded
		 * @param n
		 *            the number of decimal places
		 * @return the rounded rational number
		 */
		public Rat round(final Rat x, final Nat n) {
			final int ni = n.intValue();
			final BigDecimal decimal = BigDecimal.valueOf(x.getNumerator()).divide(BigDecimal.valueOf(x.getDenominator()));
			final long numerator = Long.parseLong(decimal.setScale(ni, BigDecimal.ROUND_HALF_UP).toPlainString().replace(".", ""));
			return ni == 0 ? Int.create(numerator) : Rat.create(numerator, (long) Math.pow(10, ni));
		}

		/**
		 * Raises a natural number to a specified power.
		 * 
		 * @param a
		 *            the base
		 * @param b
		 *            the exponent
		 * @return the value {@code a}<sup>{@code b}</sup>
		 */
		public Rat power(final Nat a, final Int b) {
			final long al = a.longValue();
			final long bl = b.longValue();
			final long result = (long) Math.pow(al, bl);
			return bl < 0 ? Rat.create(1, result) : Nat.create(result);
		}
	}

	/**
	 * Checks whether a class is a service provider.
	 * 
	 * @param clazz the class to be checked
	 * @return {@code true} if the class is a service provider
	 */
	private boolean isServiceProvider(final Class<?> clazz) {
		return !clazz.isInterface() && Service.class.isAssignableFrom(clazz);
	}

	/**
	 * Searches for all service providers in this class.
	 * 
	 * @return all service providers
	 */
	private <T extends Service> Collection<Class<T>> getServiceProviders() {
		final Class<?>[] classes = getClass().getDeclaredClasses();
		final Collection<Class<T>> services = new ArrayList<>(classes.length);
		for (final Class<?> clazz : classes) {
			if (isServiceProvider(clazz)) {
				@SuppressWarnings("unchecked")
				final Class<T> serviceClass = (Class<T>) clazz;
				services.add(serviceClass);
			}
		}
		return services;
	}

	/**
	 * Searches for a service provider with a particular name.
	 * 
	 * @param name the name of the service provider
	 * @return the service provider
	 * @throws NoServiceFoundException if no service provider was found
	 */
	private <T extends Service> Class<T> getServiceProvider(final String name) throws NoServiceFoundException {
		final Collection<Class<T>> services = getServiceProviders();
		for (Class<T> clazz : services) {
			if (clazz.getSimpleName().equals(name)) {
				return clazz;
			}
		}
		throw new NoServiceFoundException("No service found with name \"" + name + "\"!");
	}

	/**
	 * Searches in a service provider for all methods with a particular name.
	 * 
	 * @param serviceClass the service provider
	 * @param name the name of the sought-after method
	 * @return all methods in a service provider with a particular name
	 */
	private Collection<Method> getMethodsOf(final Class<Service> serviceClass, final String name) {
		final Method[] methods = serviceClass.getMethods();
		final Collection<Method> fittingMethods = new ArrayList<>(methods.length);
		for (final Method method : methods) {
			if (method.getName().equals(name)) {
				fittingMethods.add(method);
			}
		}
		return fittingMethods;
	}

	/**
	 * Searches in a service provider for all methods with a condition derived by its postcondition.
	 * 
	 * @param service the service provider
	 * @param postcondition the postcondition of the sought-after method
	 * @return all methods in a service provider with a postcondition deriving {@code postcondition}
	 */
	private Collection<Method> getMethodsOf(final Service service, final Disjunction postcondition) {
		final Method[] methods = service.getClass().getMethods();
		final Collection<Method> fittingMethods = new ArrayList<>(methods.length);
		for (final Method method : methods) {
			final Disjunction disjunction = getPostconditionOf(service, method);
			if (disjunction != null && disjunction.derives(postcondition)) {
				fittingMethods.add(method);
			}
		}
		return fittingMethods;
	}

	/**
	 * Checks whether a method fits to the given parameters and the return type.
	 * A method fits if
	 * <ol>
	 * <li>the number of formal and actual parameters is equal,</li>
	 * <li>the type of every actual parameter is equal to or
	 *     a subclass of the formal parameter (in the correct order),</li>
	 * <li>the return type of the method is equal to or a subclass of the given return type.</li>
	 * </ol>
	 * 
	 * @param method the examined method
	 * @param actualParameters the actual parameters the method should be able to handle
	 * @param returnType the demanded return type of the method
	 * @return {@code true} if the method fits to the given criteria
	 */
	private boolean methodFits(final Method method, final List<?> actualParameters, final Class<?> returnType) {
		final boolean returnTypesMatch = returnType.isAssignableFrom(method.getReturnType());
		final Class<?>[] formalParameterTypes = method.getParameterTypes();
		final boolean parameterCountsMatch = formalParameterTypes.length == actualParameters.size();
		if (returnTypesMatch && parameterCountsMatch) {
			for (int i = 0; i < formalParameterTypes.length; i++) {
				final Class<?> formalParameterType = formalParameterTypes[i];
				final Object actualParameter = actualParameters.get(i);
				if (!formalParameterType.isInstance(actualParameter)) {
					return false;
				}
			}
			return true;
		}
		return false;
	}

	/**
	 * Returns the postcondition of the given service method.
	 * 
	 * @param service the service to which the method belongs
	 * @param method the method of which the postcondition should be returned
	 * @return the postcondition or {@code null} if not found
	 */
	private Disjunction getPostconditionOf(final Service service, final Method method) {
		final Postcondition annotation = method.getAnnotation(Postcondition.class);
		if (annotation != null) {
			return service.getDisjunction(annotation.value());
		}
		return null;
	}

	/**
	 * Searches for a service matching to the given arguments. Invokes it, if
	 * found. Otherwise throws a {@link NoServiceFoundException}.
	 * 
	 * @param actualParameters
	 *            the parameters the service should be able to process
	 * @param operationName
	 *            the name of the service operation in the syntax
	 *            {@code Directory$Service.operation}
	 * @param returnType
	 *            the expected class of the return value
	 * @return the value returned by the found service operation
	 * @throws NoServiceFoundException
	 *             if no matching service was found
	 */
	public <T> T execute(final List<?> actualParameters, final String operationName, final Class<T> returnType) throws NoServiceFoundException {
		final int indexOfDollar = operationName.indexOf('$');
		final int indexOfDot = operationName.indexOf('.', indexOfDollar);
		if (indexOfDot > 0 && indexOfDollar > 0) {
			final String nameOfDirectory = operationName.substring(0, indexOfDollar);
			if (this.getClass().getSimpleName().equals(nameOfDirectory)) {
				final String nameOfService = operationName.substring(indexOfDollar + 1, indexOfDot);
				final Class<Service> serviceClass = getServiceProvider(nameOfService);
				final String nameOfMethod = operationName.substring(indexOfDot + 1);
				final Collection<Method> methods = getMethodsOf(serviceClass, nameOfMethod);
				for (final Method method : methods) {
					if (methodFits(method, actualParameters, returnType)) {
						try {
							final Service service = serviceClass.getDeclaredConstructor(this.getClass()).newInstance(this);
							@SuppressWarnings("unchecked")
							final T result = (T) method.invoke(service, actualParameters.toArray());
							return result;
						} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
								| InvocationTargetException | NoSuchMethodException | SecurityException e) {
							// a problem occurred; raise it
							throw new NoServiceFoundException(actualParameters, operationName, returnType, e);
						}
					}
				}
			}
		}
		// if we reach this point, no matching method was found
		throw new NoServiceFoundException(actualParameters, operationName, returnType);
	}

	/**
	 * Searches for a service matching to the given arguments. Invokes it, if
	 * found. Otherwise throws a {@link NoServiceFoundException}.
	 * 
	 * @param actualParameters
	 *            the parameters the service should be able to process
	 * @param d
	 *            the disjunction the service operation must be conform to
	 * @param returnType
	 *            the expected class of the return value
	 * @return the value returned by the found service operation
	 * @throws NoServiceFoundException
	 *             if no matching service was found
	 */
	public <T> T execute(final List<?> actualParameters, final Disjunction d, final Class<T> returnType) throws NoServiceFoundException {
		final Collection<Class<Service>> serviceClasses = getServiceProviders();
		for (final Class<Service> class1 : serviceClasses) {
			final Service service;
			try {
				service = class1.getDeclaredConstructor(this.getClass()).newInstance(this);
			} catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) {
				continue;
			}
			try {
				final Collection<Method> methods = getMethodsOf(service, d);
				for (final Method method : methods) {
					if (methodFits(method, actualParameters, returnType)) {
						@SuppressWarnings("unchecked")
						final T result = (T) method.invoke(service, actualParameters.toArray());
						return result;
					}
				}
			} catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) {
				// a problem occurred; raise it
				throw new NoServiceFoundException(actualParameters, d, returnType, e);
			}
		}
		// if we reach this point, no matching method was found
		throw new NoServiceFoundException(actualParameters, d, returnType);
	}
}